﻿using System;
using System.Net.Sockets;
using System.Threading;
using Data;
using Communication;
using Data.Realm;

namespace Atlantyda
{
    /// <summary>
    /// Obsluguje komunikaty usłyszane przez agenta.
    /// </summary>
    /// <param name="sender">autor komunikatu</param>
    /// <param name="massage">usłyszany komunikat</param>
    public delegate void MessageHandler(String sender, String massage);

    /// <summary>
    /// Dostarcza API do komunikacji agenta z serwerem świata.
    /// </summary>
    public class AgentAPI {

        TcpClient connection;
        int ticket;
        int hearScope;
        QueryStream stream;
        AutoResetEvent hearSomething;
        Query hearWhat;
        String message;
        MessageHandler messagehandler;
        Object threadLock;
        Thread listen;

        /// <summary>
        /// Zawiera ostatni usłyszany przez agenta komunikat.
        /// </summary>
        public String Message { 
            get { return message; }
        }

        /// <summary>
        /// Wartość logiczna opisujaca połaczenie agenta z serwerem świata.
        /// </summary>
        public Boolean Connected { 
            get { return connection.Connected; }
        }

        /// <summary>
        /// Przygotowuje informacje niezbędne do pracy.
        /// </summary>
        /// 
        /// <param name="handler">Referencja do obiektu, który bedzie obsługiwał komunikację między agentami.</param>
        public AgentAPI(MessageHandler handler)
        {
            hearSomething = new AutoResetEvent(false);
            message = "";
            messagehandler = handler;
            ticket = 0;
            threadLock = new Object();
        }

        /// <summary>
        /// Nawiązuje połączenie z serwerem i uruchamia agenta na wybranym świecie.
        /// </summary>
        /// <param name="address">Adres IP lub host serwera</param>
        /// <param name="port">Numer portu</param>
        /// <param name="username">Nazwa grupy</param>
        /// <param name="password">Hasło</param>
        /// <param name="worldname">Nazwa świata</param>
        /// <param name="agentname">Imie agenta</param>
        /// <returns>Parametry podłaczanego świata.</returns>
        /// <exception cref="Exception">Połączenie zostało zerwane</exception>
        public WorldParameters Connect(String address, int port, String username, String password, String worldname, string agentname)
        {
            lock(threadLock) {
                try {
                    connection = new TcpClient(); 
                    connection.Connect(address, port);
                }
                catch (Exception e) { 
                    Exception ee = new Exception("Błąd połączenia. " + e.Message, e);
                    throw ee;
                }
                stream = new QueryStream(connection.GetStream());
                Query query = new Query(Command.Connect, 0, username, password, worldname,agentname);
                stream.Write(query);
                query = stream.Read(true);
                switch(query.command) {
                    case Command.Error: throw new NonCriticalException("Błąd autoryzacji.");
                    case Command.Disconnect: throw new NonCriticalException("Świat nie istnieje.");
                    case Command.WorldInfo:
                        listen = new Thread(Listen);
                        listen.Start();
                        ticket = (int)query.parameter[1];
                        hearScope = (query.parameter[0] as WorldParameters).hearScope;
                        return query.parameter[0] as WorldParameters;
                    default: throw new Exception("Nieoczekiwany błąd transmisji.");
                }
            }
        }

        /// <summary>
        /// Rozłącza agenta z serwerem.
        /// </summary>
        public void Disconnect() {
            try {
                if(connection.Connected) {
                    listen.Abort();
                    Query query = new Query(Command.Disconnect, ticket);
                    stream.Write(query);
                    stream.Close();
                    connection.Close();
                }
            }
            catch(System.IO.IOException) { }
            finally {
                ticket = 0;
            }
        }

        /// <summary>
        /// Pobiera informacje o polach znajdujących sie w zasięgu widzenia agenta.
        /// </summary>
        /// <returns>Pola w zasiegu widzenia agenta.</returns>
        /// <exception cref="InvalidOperationException">Agent nie podłączony do świata</exception>
        public OrientedField[] Look() {
            lock(threadLock) {
                try {
                    if(!Connected) throw new InvalidOperationException("Agent nie podłączony do świata.");
                    Query query = new Query(Command.Look, ticket);
                    stream.Write(query);
                    hearSomething.WaitOne();
                    if(hearWhat == null) throw new TimeoutException();
                    return hearWhat.parameter as OrientedField[];
                }
                catch(System.IO.IOException) {
                    Disconnect();
                    throw new Exception("Połączenie zostało zerwane.");
                }
            }
        }

        /// <summary>
        /// Agent obraca sie w lewo.
        /// </summary>
        /// <returns>Fałsz jeśli agent ma niewystrczającą ilość energi.</returns>
        /// <exception cref="InvalidOperationException">Agent nie podłączony do świata</exception>
        public bool RotateLeft() {
            lock(threadLock) {
                try {
                    if(!Connected) throw new InvalidOperationException("Agent nie podłączony do świata.");
                    Query query = new Query(Command.RotateLeft, ticket);
                    stream.Write(query);
                    hearSomething.WaitOne();
                    return (hearWhat.command == Command.Confirm ? true : false);
                }
                catch(System.IO.IOException) {
                    Disconnect();
                    throw new Exception("Połączenie zostało zerwane.");
                }
            }
        }

        /// <summary>
        /// Agent obraca sie w prawo.
        /// </summary>
        /// <returns>Fałsz jeśli agent ma niewystrczającą ilość energi.</returns>
        /// <exception cref="InvalidOperationException">Agent nie podłączony do świata</exception>
        public bool RotateRight() {
            lock (threadLock) {
                try {
                    if(!Connected) throw new InvalidOperationException("Agent nie podłączony do świata.");
                    Query query = new Query(Command.RotateRight, ticket);
                    stream.Write(query);
                    hearSomething.WaitOne();
                    return (hearWhat.command == Command.Confirm ? true : false);
                }
                catch(System.IO.IOException) {
                    Disconnect();
                    throw new Exception("Połączenie zostało zerwane.");
                }
            }
        }

        /// <summary>
        /// Agent wykrzykuje komunikat.
        /// </summary>
        /// <param name="sentence">Treść komunikatu.</param>
        /// <param name="range">Zasięg komunikatu.</param>
        /// <returns>Fałsz jeśli agent ma niewystrczającą ilość energi.</returns>
        /// <exception cref="InvalidOperationException">Agent nie podłączony do świata</exception>
        public bool Speak(String sentence, Double range) {
            lock(threadLock) {
                try {
                    if(!Connected) throw new InvalidOperationException("Agent nie podłączony do świata.");
                    if(sentence.Length > 1024) throw new ArgumentOutOfRangeException("sentence", "Komunikat jest zbyt długi.");
                    if(range < 0 || 1 < range) throw new ArgumentOutOfRangeException("range", "Wartość z poza zakresu.");
                    Query query = new Query(Command.Speak, ticket, sentence, (int)Math.Ceiling(hearScope * range));
                    stream.Write(query);
                    hearSomething.WaitOne();
                    return ((hearWhat != null && hearWhat.command == Command.Confirm) ? true : false);
                }
                catch(System.IO.IOException) {
                    Disconnect();
                    throw new Exception("Połączenie zostało zerwane.");
                }
            }
        }

        /// <summary>
        /// Agent robi krok naprzód.
        /// </summary>
        /// <returns>Fałsz jeśli agent ma niewystrczającą ilość energi.</returns>
        /// <exception cref="InvalidOperationException">Agent nie podłączony do świata</exception>
        public bool StepForward()
        {
            lock (threadLock)
            {
                try
                {
                    if (!Connected) throw new InvalidOperationException("Agent nie podłączony do świata.");
                    Query query = new Query(Command.StepForward, ticket);
                    stream.Write(query);
                    hearSomething.WaitOne();
                    return (hearWhat.command == Command.Confirm ? true : false);
                }
                catch (System.IO.IOException)
                {
                    Disconnect();
                    throw new Exception("Połączenie zostało zerwane.");
                }
            }
        }

        /// <summary>
        /// Agent ładuje energie ze zródła.
        /// </summary>
        /// <exception cref="InvalidOperationException">Agent nie podłączony do świata</exception>
        public int Recharge() {
            lock(threadLock) {
                try {
                    if(!Connected) throw new InvalidOperationException("Agent nie podłączony do świata.");
                    Query query = new Query(Command.Recharge, ticket);
                    stream.Write(query);
                    hearSomething.WaitOne();
                    return (int)hearWhat.parameter[0];
                }
                catch(System.IO.IOException) {
                    Disconnect();
                    throw new Exception("Połączenie zostało zerwane.");
                }
            }
        }

        private void Listen()
        {
            while(connection.Connected) {
                Query answer = stream.Read(true);
                //Thread.Sleep(10);
                if(answer == null) continue;
                hearWhat = null; 
                if(answer.ticket == this.ticket)
                    switch(answer.command) {
                        case Command.Speak:
                            messagehandler(answer.parameter[0] as String, answer.parameter[1] as String);
                            break;
                        case Command.Disconnect:
                            if(Connected) connection.Close();
                            break;
                        case Command.Look:
                            continue;
                        default:
                            hearWhat = answer;
                            hearSomething.Set();
                            break;
                    }
            }
        }
    }
}
